Introduction au Streaming adaptatif

Luc Trudeau

Au menu:

Logiciels requis

FFMPEG (Outil de codage)

https://ffmpeg.org/ffmpeg.html

GStreamer (Diffusion)

http://gstreamer.freedesktop.org/

Séquence Vidéo

http://www.caminandes.com/

Streaming Adaptatif

Idée de base:


In [ ]:
!ffmpeg -i LlamaDrama.mp4 -movflags faststart -b:v 256000 -maxrate 256000 -x264opts "fps=24:keyint=48:min-keyint=48:no-scenecut" -hls_list_size 0 -hls_time 4 -hls_base_url http://192.168.3.14:8000/low/ low/LlamaDrama.m3u8

Llama Drama Low (1920x1080)

256 kbits/secondes


In [ ]:
!ffmpeg -i LlamaDrama.mp4 -movflags faststart -b:v 512000 -maxrate 512000 -x264opts "fps=24:keyint=48:min-keyint=48:no-scenecut" -hls_list_size 0 -hls_time 4 -hls_base_url http://192.168.3.14:8000/medium/ medium/LlamaDrama.m3u8

Llama Drama Medium (1920x1080)

512 kbits/secondes


In [ ]:
!ffmpeg -i LlamaDrama.mp4 -movflags faststart -b:v 1024000 -maxrate 1024000 -x264opts "fps=24:keyint=48:min-keyint=48:no-scenecut" -hls_list_size 0 -hls_time 4 -hls_base_url http://192.168.3.14:8000/high/ high/LlamaDrama.m3u8

Llama Drama High (1920x1080)

1024 kbits/secondes

Le Manifeste

HLS utilise le format m3u8

/LlamaDrama.m3u8

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=256000
http://localhost:8000/low/LlamaDrama.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=512000
http://localhost:8000/medium/LlamaDrama.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1024000
http://localhost:8000/high/LlamaDrama.m3u8

In [1]:
from collections import namedtuple
from io import BytesIO
from requests import get
import m3u8
from time import time
from io import BytesIO
from subprocess import call

In [2]:
Stream = namedtuple('Stream',['bandwidth', 'uri'])

Playlist

Chaque playlist pointe les segments de la séquence

/high/LlamaDrama.m3u8

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.000000,
http://localhost:8000/high/LlamaDrama0.ts
#EXTINF:4.000000,
http://localhost:8000/high/LlamaDrama1.ts
#EXTINF:4.000000,
http://localhost:8000/high/LlamaDrama2.ts
#EXTINF:4.000000,
http://localhost:8000/high/LlamaDrama3.ts
...
#EXTINF:4.000000,
http://localhost:8000/high/LlamaDrama21.ts
#EXTINF:1.875000,
http://localhost:8000/high/LlamaDrama22.ts
#EXT-X-ENDLIST

Client HLS

La classe HLS permet d'itérer a traver les segments.

Le temps est mesuré, lors du téléchargment du segment.

En combinant le temps et la taille du fichier, on obtient la vitesse.

Le StreamEngine choisit le stream approprié en fonction de la vitesse.

@startuml skinparam style strictuml skinparam dpi 300

class HLS << iterable >> { ByteIO next() }

class StreamEngine { Stream selectStream(speed)
}

HLS -right-> StreamEngine

@enduml

Classe HLS

Le patron Iterator est utilisé pour itérer à travers l'ensemble des segments de la séquence.


In [3]:
class HLS:
    
    speed = 0 # Bits / second
    i = 0
    
    def __init__(self, uri):
        self.selector = StreamEngine(uri)
    
    def __iter__(self):
        return self
    
    def __next__(self):
        stream = self.selector.selectStream(self.speed)
        
        if self.i < len(stream.segments):
            startTime = time()
            buf = getSegment(stream.segments[self.i])
            self.speed = round((buf.getbuffer().nbytes*8) / (time() - startTime))
            print('%d bits/s' %self.speed)
            self.i += 1
            return buf
        else:
            raise StopIteration

StreamEngine

Le StreamEngine choisi le prochain segment en fonction de la vitesse de transfert


In [4]:
class StreamEngine:
    
    currentStream = None
    streamM3 = None
    streams = None
    
    def __init__(self, uri):
        self.streams = sorted([Stream(playlist.stream_info.bandwidth, playlist.uri) 
                               for playlist in m3u8.load(uri).playlists])
        self.currentStream = self.streams[0]
        self.streamM3 = m3u8.load(self.currentStream.uri)
    
    def selectStream(self, speed):
        newStream = self.currentStream
        
        for stream in self.streams:
            if stream.bandwidth < speed:
                newStream = stream
            else:
                break
        
        if newStream != self.currentStream:
            self.currentStream = newStream
            self.streamM3 = m3u8.load(newStream.uri)
            print('Changing Streams: New BitRate %d' %newStream.bandwidth)
        
        return self.streamM3

In [5]:
def getSegment(segment):
    buf = BytesIO()
    r = get(segment.uri, stream=True)
    for chunk in r.iter_content(chunk_size=2048): 
        if chunk:
            buf.write(chunk)
    return buf

Exemple réel

Le contenu des segments reçu par HLS est "pipé" dans GStreamer

Même, si GStreamer possède un buffer d'entrée, nous utilisons quand même un buffer interne pour ne pas blocker un téléchargement lorsque le buffer de GStreamer est plein.

L'appel de player.stdin.write étant bloquant, ceci va servir de mécanisme de controlle de flux pour les requêtes HLS.


In [ ]:
from subprocess import Popen, PIPE, STDOUT

hls = HLS('http://192.168.3.14:8000/LlamaDrama.m3u8')
player = Popen("/usr/local/bin/gst-play-1.0 fd://0".split(), stdout=PIPE, stdin=PIPE)

for segment in hls:
    player.stdin.write(segment.getvalue())